﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace XmlSpecParser
{
	public class XmlSpecParser
	{
        public class Options
        {
            public string Prefix;
        }

		public XmlSpecData Parse(string file, string overridesFile, Options options)
		{
			XmlSpecData data = new XmlSpecData();
            			
			XDocument doc = XDocument.Load(file);
            this.ParseEnumGroups(doc, data, options);
            this.ParseEnums(doc, data, options);
            this.ParseCommands(doc, data, options);
            this.ParseFeatures(doc, data, options);
            this.ParseExtensions(doc, data, options);

            if (overridesFile != null)
            {
                doc = XDocument.Load(overridesFile);
                this.ParseOverrides(doc, data);
            }

            data.EnumGroups.Sort((x, y) => x.Name != null && y.Name != null ? x.Name.CompareTo(y.Name) : 0);
            data.Enums.Sort((x, y) => x.Name != null && y.Name != null ? x.Name.CompareTo(y.Name) : 0);
            data.Commands.Sort((x, y) => x.Name != null && y.Name != null ? x.Name.CompareTo(y.Name) : 0);

			return data;
		}

        private void ParseEnumGroups(XDocument doc, XmlSpecData data, Options options)
        {
            var groupsNode = doc.Root.Element("groups");

            if (groupsNode == null)
                return;

            foreach (var enumGroupNode in groupsNode.Elements())
            {
                XmlEnumGroupData enumGroupData = new XmlEnumGroupData()
                {
                    Name = enumGroupNode.GetAttributeValue("name")
                };

                foreach (var enumNode in enumGroupNode.Elements())
                {
                    enumGroupData.Enums.Add(enumNode.GetAttributeValue("name"));
                }

                data.EnumGroups.Add(enumGroupData);
            }
        }

        private void ParseEnums(XDocument doc, XmlSpecData data, Options options)
		{
			foreach (var enumSetNode in doc.Root.Elements("enums"))
			{
				foreach (var enumNode in enumSetNode.Elements())
				{
					XmlEnumData enumData = new XmlEnumData()
					{
						Group = enumSetNode.GetAttributeValue("group"),
						Type = enumSetNode.GetAttributeValue("type"),
						Vendor = enumSetNode.GetAttributeValue("vendor"),
						Value = enumNode.GetAttributeValue("value"),
						Name = enumNode.GetAttributeValue("name")
					};

					if (enumData.Name != null && data.Enums.SingleOrDefault(x => x.Name == enumData.Name) == null)
						data.Enums.Add(enumData);
				}
			}
		}

        private string EnsureCommandNameHasPrefix(string name, Options options)
        {
            if (!name.StartsWith(options.Prefix))
            {
                name = options.Prefix + name;
            }

            return name;
        }

        private void ParseCommands(XDocument doc, XmlSpecData data, Options options)
		{
			foreach (var commandSetNode in doc.Root.Elements("commands"))
			{
				foreach (var commandNode in commandSetNode.Elements())
				{
					XmlCommandData commandData = new XmlCommandData()
					{
                        Name = this.EnsureCommandNameHasPrefix(commandNode.GetElementValue("proto", "name"), options),
						ReturnType = commandNode.GetElementValue("proto", "ptype"),
						ReturnTypeGroup = commandNode.GetElementAttributeValue("proto", "group"),
						Alias = commandNode.GetElementAttributeValue("alias", "name")
					};
									
					if (commandData.ReturnType == null)
						commandData.ReturnType = commandNode.Element("proto").GetTexts().First().Trim();

					// "void *" => "void*"
					if (commandData.ReturnType.EndsWith(" *"))
						commandData.ReturnType = commandData.ReturnType.Substring(0, commandData.ReturnType.Length - 2) + "*";

					foreach (var commandParamNode in commandNode.Elements("param"))
					{
						XmlCommandParamData commandParamData = new XmlCommandParamData()
						{
							Name = commandParamNode.GetElementValue("name"),
							Type = commandParamNode.GetElementValue("ptype"),
							TypeGroup = commandParamNode.GetAttributeValue("group"),
							Length = commandParamNode.GetAttributeValue("len")
						};

						if (commandParamData.Type == null)
						{
							commandParamData.Type = commandParamNode.GetTexts().First().Trim();
						}
						else
						{
							var texts = commandParamNode.GetTexts();

							if (texts.Count() == 1 &&
								texts.First().Contains("*"))
							{
								commandParamData.Type = commandParamData.Type + new string('*', texts.First().Count(x => x == '*'));
							}
							else if (
								texts.Count() == 2 &&
								texts.First().Trim() == "const" &&
								texts.Last().Contains("*"))
							{
								commandParamData.Type = "const " + commandParamData.Type + new string('*', texts.Last().Count(x => x == '*'));
							}
						}

						// "const void *" => "const void*"
						if (commandParamData.Type.EndsWith(" *"))
							commandParamData.Type = commandParamData.Type.Substring(0, commandParamData.Type.Length - 2) + "*";

						commandData.Params.Add(commandParamData);
					}

					data.Commands.Add(commandData);
				}
			}
		}

        private void ParseFeatures(XDocument doc, XmlSpecData data, Options options)
		{
			foreach (var featureNode in doc.Root.Elements("feature"))
			{
				string[] versionParts = featureNode.GetAttributeValue("number").Split('.');
				int versionMajor = int.Parse(versionParts[0]);
				int versionMinor = int.Parse(versionParts[1]);

				var featureData = new XmlFeatureData()
				{
					Api = featureNode.GetAttributeValue("api"),
					Name = featureNode.GetAttributeValue("name"),
					VersionMajor = versionMajor,
					VersionMinor = versionMinor
				};

				foreach (var enumNode in featureNode.Descendants("enum"))
				{
					featureData.Enums.Add(enumNode.GetAttributeValue("name"));
				}

				foreach (var commandNode in featureNode.Descendants("command"))
				{
					featureData.Commands.Add(this.EnsureCommandNameHasPrefix(commandNode.GetAttributeValue("name"), options));
				}

				data.Features.Add(featureData);
			}
		}

        private void ParseExtensions(XDocument doc, XmlSpecData data, Options options)
        {
            foreach (var extensionNode in doc.Root.Element("extensions").Elements("extension"))
            {                
                var extensionData = new XmlExtensionData()
                {
                    Name = extensionNode.GetAttributeValue("name"),                    
                };

                foreach (var enumNode in extensionNode.Descendants("require").Descendants("enum"))
                {
                    extensionData.Enums.Add(enumNode.GetAttributeValue("name"));
                }

                foreach (var commandNode in extensionNode.Descendants("require").Descendants("command"))
                {
                    extensionData.Commands.Add(commandNode.GetAttributeValue("name"));
                }

                data.Extensions.Add(extensionData);
            }
        }

        private void ParseOverrides(XDocument doc, XmlSpecData data)
        {
            foreach (var groupNode in doc.Root.Element("groups").Elements())
            {
                var enumGroup = data.EnumGroups.SingleOrDefault(x => x.Name == groupNode.GetAttributeValue("name"));

                if (enumGroup == null)
                {
                    enumGroup = new XmlEnumGroupData()
                    {
                        Name = groupNode.GetAttributeValue("name")
                    };

                    data.EnumGroups.Add(enumGroup);
                }

                if (groupNode.Element("enums") != null)
                {
                    foreach (string @enum in groupNode.Element("enums").Value.Split(',').Select(x => x.Trim()))
                    {
                        if (!enumGroup.Enums.Contains(@enum))
                        {
                            enumGroup.Enums.Add(@enum);
                        }
                    }
                }
            }

            foreach (var commandNode in doc.Root.Element("commands").Elements())
            {
                var command = data.Commands.Single(x => x.Name == commandNode.GetAttributeValue("name"));
                
                foreach (var commandParamNode in commandNode.Elements("param"))
                {
                    var param = command.Params.Single(x => x.Name == commandParamNode.GetAttributeValue("name"));

                    string typeGroup = commandParamNode.GetAttributeValue("group");

                    if (typeGroup != null)
                        param.TypeGroup = typeGroup;
                }
            }
        }
	}
}
